package cn.jg.framework.web.filter;

import cn.jg.framework.common.util.WebUtils;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.UUID;

import static cn.jg.framework.common.constant.CharacterConstant.REQUEST_ID_HEADER;

/**
 * @author fanfeveryahoo@gmail.com
 * @date 2019/3/15
 */

@Slf4j
public abstract class BaseLogRequestFilter extends OncePerRequestFilter implements Ordered {

	private ObjectMapper objectMapper;
	private long slowRequestDurationWarn;
	private long slowRequestDurationError;
	private int paymentLength = 2048;

	private boolean customRequestIdSuffix;

	private String ignoreUriPrefix = "/actuator";

	private BaseLogRequestFilter() {

	}

	public BaseLogRequestFilter(ObjectMapper objectMapper) {
		this.objectMapper = objectMapper;
		this.slowRequestDurationWarn = 1_000L;
		this.slowRequestDurationError = 5_000L;
	}

	public BaseLogRequestFilter(ObjectMapper objectMapper, boolean customRequestIdSuffix) {
		this.objectMapper = objectMapper;
		this.slowRequestDurationWarn = 1_000L;
		this.slowRequestDurationError = 5_000L;
		this.customRequestIdSuffix = customRequestIdSuffix;
	}

	public BaseLogRequestFilter(ObjectMapper objectMapper, long slowRequestDurationWarn, long slowRequestDurationError, int paymentLength) {
		this.objectMapper = objectMapper;
		this.slowRequestDurationWarn = slowRequestDurationWarn;
		this.slowRequestDurationError = slowRequestDurationError;
		this.paymentLength = paymentLength;
	}

	@Override
	public int getOrder() {
		return Ordered.LOWEST_PRECEDENCE - 16;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
		try {
			final String token;
			if (StringUtils.isNoneBlank(httpServletRequest.getHeader(REQUEST_ID_HEADER))) {
				if (customRequestIdSuffix) {
					token = httpServletRequest.getHeader(REQUEST_ID_HEADER) + "-" + StringUtils.substringBefore(UUID.randomUUID().toString(), "-");
				} else {
					token = httpServletRequest.getHeader(REQUEST_ID_HEADER);
				}
			} else {
				token = UUID.randomUUID().toString().replace("-", "").toLowerCase();
			}
			MDC.put(REQUEST_ID_HEADER, token);
			httpServletResponse.addHeader(REQUEST_ID_HEADER, token);
			ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpServletRequest);
			ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpServletResponse);
			long startTime = System.currentTimeMillis();

			filterChain.doFilter(requestWrapper, responseWrapper);

			String requestPayload = getPayLoad(requestWrapper.getContentType(), requestWrapper.getContentAsByteArray(), httpServletRequest.getCharacterEncoding());
			String responsePayload = getPayLoad(requestWrapper.getContentType(), responseWrapper.getContentAsByteArray(), httpServletResponse.getCharacterEncoding());
			responseWrapper.copyBodyToResponse();

			long duration = System.currentTimeMillis() - startTime;

			if (httpServletResponse.getStatus() >= HttpStatus.INTERNAL_SERVER_ERROR.value()) {
				log.error("logRequestStatus5xx {}", objectMapper.writeValueAsString(RequestDTO.build(httpServletRequest, httpServletResponse, duration, requestPayload, responsePayload)));
			} else if (duration > slowRequestDurationError) {
				log.error("logRequestSlowError {}", objectMapper.writeValueAsString(RequestDTO.build(httpServletRequest, httpServletResponse, duration, requestPayload, responsePayload)));
			} else if (duration > slowRequestDurationWarn) {
				log.warn("logRequestSlowWarn {}", objectMapper.writeValueAsString(RequestDTO.build(httpServletRequest, httpServletResponse, duration, requestPayload, responsePayload)));
			} else if (httpServletResponse.getStatus() >= HttpStatus.BAD_REQUEST.value()) {
				log.info("logRequestStatus4xx {}", objectMapper.writeValueAsString(RequestDTO.build(httpServletRequest, httpServletResponse, duration, requestPayload, responsePayload)));
			} else {
				if (!StringUtils.startsWith(httpServletRequest.getRequestURI(), ignoreUriPrefix)) {
					log.info("logRequest {}", objectMapper.writeValueAsString(RequestDTO.build(httpServletRequest, httpServletResponse, duration, requestPayload, responsePayload)));
				}
			}
		} finally {
			MDC.remove(REQUEST_ID_HEADER);
		}
	}


	private String getPayLoad(String contentType, byte[] buf, String characterEncoding) {
		if (isBinaryContent(contentType)) {
			return StringUtils.EMPTY;
		}
		String payload = StringUtils.EMPTY;
		if (buf.length > 0) {
			int length = Math.min(buf.length, paymentLength);
			String ellipsis = buf.length > length ? " ... " : StringUtils.EMPTY;
			try {
				payload = StringUtils.normalizeSpace(new String(buf, 0, length, characterEncoding)) + ellipsis;
			} catch (UnsupportedEncodingException ex) {
				payload = "[unknown]";
			}
		}
		return payload;
	}

	private boolean isBinaryContent(final String contentType) {
		return StringUtils.startsWithAny(contentType, "image", "video", "audio", "multipart/form-data");
	}

	@Data
	@Builder
	@JsonIgnoreProperties(ignoreUnknown = true)
	@JsonInclude(value = JsonInclude.Include.NON_NULL)
	private static class RequestDTO {
		Integer status;
		Long duration;
		String uri;
		String queryParams;
		String method;
		String protocol;
		Map<String, String> headers;
		@JsonRawValue
		String requestBody;
		@JsonRawValue
		String responseBody;
		String requestIp;


		static RequestDTO build(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, long duration, String requestPayload, String responsePayload) {
			return RequestDTO.builder()
					.status(httpServletResponse.getStatus())
					.duration(duration)
					.uri(httpServletRequest.getRequestURI())
					.queryParams(httpServletRequest.getQueryString())
					.method(httpServletRequest.getMethod())
					.protocol(httpServletRequest.getProtocol())
					.requestIp(WebUtils.getRemoteAddr(httpServletRequest))
					.headers(ImmutableMap.of("Accept:", httpServletRequest.getHeader("Accept") + StringUtils.EMPTY, "Content-Type:", httpServletRequest.getHeader("Content-Type") + StringUtils.EMPTY))
					.requestBody(requestPayload)
					.responseBody(responsePayload)
					.build();
		}
	}
}
